/*
Copyright 2021.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controllers

import (
	"fmt"
	"gitee.com/wayjin/fabricsdk-go/caclient"
	"github.com/go-logr/logr"
	"github.com/prometheus/common/log"
	"golang.org/x/net/context"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/intstr"
	"time"

	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"

	fabricv1alpha1 "gitee.com/wayjin/fabric-baas-operator/api/v1alpha1"
)

// PeerOrgReconciler reconciles a PeerOrg object
type PeerOrgReconciler struct {
	client.Client
	Log    logr.Logger
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=fabric.example.com,resources=peerorgs,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=fabric.example.com,resources=peerorgs/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=fabric.example.com,resources=peerorgs/finalizers,verbs=update
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the PeerOrg object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
func (r *PeerOrgReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = r.Log.WithValues("peerorg", req.NamespacedName)

	// your logic here
	peerOrgs := &fabricv1alpha1.PeerOrg{}
	err := r.Get(ctx, req.NamespacedName, peerOrgs)
	if err != nil {
		if errors.IsNotFound(err) {
			log.Info("PeerOrg not found, Ignoring since object must be deleted")
			return ctrl.Result{}, nil
		}
		log.Error("Fail to get PeerOrg: ", err)
		return ctrl.Result{}, err
	}
	err = r.createOrg(ctx, peerOrgs)
	if err != nil {
		log.Error("Fail to init peerOrg: ", err)
		return ctrl.Result{}, err
	}

	err = r.createPeer(ctx, peerOrgs)
	if err != nil {
		log.Errorf("Fail to create peer: %v", err)
		return ctrl.Result{}, err
	}

	return ctrl.Result{}, nil
}

func (r *PeerOrgReconciler) createOrg(ctx context.Context, peerOrg *fabricv1alpha1.PeerOrg) error {
	orgSecret := &corev1.Secret{}
	err := r.Get(ctx, types.NamespacedName{Name: peerOrg.Name + "-admin", Namespace: peerOrg.Namespace}, orgSecret)
	if err != nil {
		if errors.IsNotFound(err) {
			log.Infof("Not found admin secret of peerorg %s, creating", peerOrg.Name)
			orgCert, err := getFabricCertificate("Admin@org1", "12345", "Admin@org1", caclient.ID_ADMIN)
			if err != nil {
				log.Error("Fail to get certificate from CA: ", err)
				return err
			}
			secret := r.certSecretForOrg(peerOrg, orgCert)
			err = r.Create(ctx, secret)
			if err != nil {
				log.Error("Fail to create cert secret: ", err)
				return err
			}
			log.Info("Create secret for peerorg success")
		} else {
			log.Error("Fail to get admin secret of peerOrgs: ", err)
			return err
		}
	} else {
		log.Infof("Secret %s-admin exist", peerOrg.Name)
	}

	return nil
}

func (r *PeerOrgReconciler) createPeer(ctx context.Context, peerOrg *fabricv1alpha1.PeerOrg) error {
	peerID := peerOrg.Name + "-peer0"
	peerSecret := &corev1.Secret{}
	err := r.Get(ctx, types.NamespacedName{Name: peerID, Namespace: peerOrg.Namespace}, peerSecret)
	if err != nil {
		if errors.IsNotFound(err) {
			peerCert, err := getFabricCertificate("peer0", "12345", getPeerHostname(peerOrg, "peer0"), caclient.ID_PEER)
			if err != nil {
				log.Errorf("Fail to apply certificates from CA: %s", err)
				return err
			}
			secret := r.certSecretForPeer(peerOrg, peerCert, peerID)
			err = r.Create(ctx, secret)
			if err != nil {
				log.Errorf("Fail to create secret of peer: %v", err)
				return err
			}
			log.Infof("Create secret of %s success", peerID)
		} else {
			log.Errorf("Fail to get secret of %s with error: %v", peerID, err)
			return err
		}
	}

	peerService := &corev1.Service{}
	err = r.Get(ctx, types.NamespacedName{Name: peerID, Namespace: peerOrg.Namespace}, peerService)
	if err != nil {
		if errors.IsNotFound(err) {
			log.Infof("Not found peer service of %s, creating", peerID)
			service := r.serviceForPeer(peerOrg, peerID)
			err = r.Create(ctx, service)
			if err != nil {
				log.Errorf("Fail to create %s service with error: %v", peerID, err)
				return err
			}
			log.Infof("Create %s service success", peerID)
		} else {
			log.Errorf("Fail to get service of %s with error: %v", peerID, err)
			return err
		}
	} else {
		// TODO
		log.Infof("Peer %s service exist, ignore it", peerID)
	}

	time.Sleep(2 * time.Second) // test wait for secret created

	peerDeploy := &appsv1.Deployment{}
	err = r.Get(ctx, types.NamespacedName{Name: peerID, Namespace: peerOrg.Namespace}, peerDeploy)
	if err != nil {
		if errors.IsNotFound(err) {
			log.Infof("Not found deployment of %s, creating", peerID)
			deploy := r.deploymentForPeer(peerOrg, peerID)
			err := r.Create(ctx, deploy)
			if err != nil {
				log.Errorf("Fail to create deployment of %s with error: %v", peerID, err)
				return err
			}
			log.Infof("Create deployment of %s success", peerID)
		} else {
			log.Errorf("Fail to get deployment of %s with error: %s", peerID, err)
			return err
		}
	} else {
		// TODO
		log.Infof("Deployment %s exist, ignore it", peerID)
	}

	// for update
	return nil
}

func (r *PeerOrgReconciler) deploymentForCA(peerOrg *fabricv1alpha1.PeerOrg, caID string) *appsv1.Deployment {
	replica := int32(1)
	labels := r.labelsForCA(caID)
	dep := &appsv1.Deployment{
		ObjectMeta: r.objectMetaForCA(peerOrg),
		Spec: appsv1.DeploymentSpec{
			Replicas: &replica,
			Selector: &metav1.LabelSelector{
				MatchLabels: labels,
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: labels,
				},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{r.containerForCA(peerOrg, caID)},
				},
			},
		},
	}
	ctrl.SetControllerReference(peerOrg, dep, r.Scheme)
	return dep
}

func (r *PeerOrgReconciler) deploymentForPeer(peerOrg *fabricv1alpha1.PeerOrg, peerID string) *appsv1.Deployment {
	labels := r.labelsForPeer(peerID)
	replica := int32(1)
	dep := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      peerID,
			Namespace: peerOrg.Namespace,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: &replica,
			Selector: &metav1.LabelSelector{
				MatchLabels: labels,
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: labels,
				},
				Spec: corev1.PodSpec{
					Volumes:    r.podVolumeForPeer(peerOrg, peerID),
					Containers: []corev1.Container{r.containerForPeer(peerOrg)},
				},
			},
		},
	}
	ctrl.SetControllerReference(peerOrg, dep, r.Scheme)
	return dep
}

func (r *PeerOrgReconciler) waitForSecretCreated(peerID string, timeout time.Duration) {

}
func (r *PeerOrgReconciler) serviceForCA(peerOrg *fabricv1alpha1.PeerOrg) *corev1.Service {
	label := r.labelsForCA(peerOrg.Name + "-ca")
	svc := &corev1.Service{
		ObjectMeta: r.objectMetaForCA(peerOrg),
		Spec: corev1.ServiceSpec{
			Type: corev1.ServiceTypeClusterIP,
			Ports: []corev1.ServicePort{
				{
					Name:       peerOrg.Name + "-ca",
					Protocol:   corev1.ProtocolTCP,
					Port:       7054,
					TargetPort: intstr.FromInt(7054),
				},
			},
			Selector: label,
		},
	}

	ctrl.SetControllerReference(peerOrg, svc, r.Scheme)
	return svc
}

func (r *PeerOrgReconciler) serviceForPeer(peerOrg *fabricv1alpha1.PeerOrg, peerID string) *corev1.Service {
	label := r.labelsForPeer(peerID)
	svc := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      peerID,
			Namespace: peerOrg.Namespace,
		},
		Spec: corev1.ServiceSpec{
			Type: corev1.ServiceTypeClusterIP,
			Ports: []corev1.ServicePort{
				{
					Name:       "grpc",
					Protocol:   corev1.ProtocolTCP,
					Port:       7051,
					TargetPort: intstr.FromInt(7051),
				},
			},
			Selector: label,
		},
	}

	ctrl.SetControllerReference(peerOrg, svc, r.Scheme)
	return svc
}

func (r *PeerOrgReconciler) certSecretForOrg(peerOrg *fabricv1alpha1.PeerOrg, cert *CryptoCert) *corev1.Secret {
	secret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      peerOrg.Name + "-admin",
			Namespace: peerOrg.Namespace,
		},
		Type: corev1.SecretTypeOpaque,
		Data: map[string][]byte{
			"cacert":    cert.cacert,
			"tlscacert": cert.tlscacert,
			"signcert":  cert.signcert,
			"keycert":   cert.key,
			"servercrt": cert.servercrt,
			"serverkey": cert.serverkey,
		},
	}
	ctrl.SetControllerReference(peerOrg, secret, r.Scheme)
	return secret
}

func (r *PeerOrgReconciler) certSecretForPeer(peerOrg *fabricv1alpha1.PeerOrg, cert *CryptoCert, peerID string) *corev1.Secret {
	secret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      peerID,
			Namespace: peerOrg.Namespace,
		},
		Type: corev1.SecretTypeOpaque,
		Data: map[string][]byte{
			"signcert":  cert.signcert,
			"keycert":   cert.key,
			"cacert":    cert.cacert,
			"tlscacert": cert.tlscacert,
			"servercrt": cert.servercrt,
			"serverkey": cert.serverkey,
			"config":    []byte(mspConfig),
		},
	}
	ctrl.SetControllerReference(peerOrg, secret, r.Scheme)
	return secret
}

func (r *PeerOrgReconciler) podVolumeForPeer(peerOrg *fabricv1alpha1.PeerOrg, peerID string) []corev1.Volume {
	volumes := []corev1.Volume{
		{
			Name: "signcert",
			VolumeSource: corev1.VolumeSource{
				Secret: &corev1.SecretVolumeSource{
					SecretName: peerID,
					Items:      []corev1.KeyToPath{{Key: "signcert", Path: "cert.pem"}},
				},
			},
		},
		{
			Name: "keycert",
			VolumeSource: corev1.VolumeSource{
				Secret: &corev1.SecretVolumeSource{
					SecretName: peerID,
					Items:      []corev1.KeyToPath{{Key: "keycert", Path: "priv_sk"}},
				},
			},
		},
		{
			Name: "cacert",
			VolumeSource: corev1.VolumeSource{
				Secret: &corev1.SecretVolumeSource{
					SecretName: peerID,
					Items:      []corev1.KeyToPath{{Key: "cacert", Path: "cacert.pem"}},
				},
			},
		},
		{
			Name: "tlscacert",
			VolumeSource: corev1.VolumeSource{
				Secret: &corev1.SecretVolumeSource{
					SecretName: peerID,
					Items:      []corev1.KeyToPath{{Key: "tlscacert", Path: "tlscacert.pem"}},
				},
			},
		},
		{
			Name: "admincerts",
			VolumeSource: corev1.VolumeSource{
				EmptyDir: &corev1.EmptyDirVolumeSource{},
			},
		},
		{
			Name: "cacrt",
			VolumeSource: corev1.VolumeSource{
				Secret: &corev1.SecretVolumeSource{
					SecretName: peerID,
					Items:      []corev1.KeyToPath{{Key: "tlscacert", Path: "ca.crt"}},
				},
			},
		},
		{
			Name: "servercrt",
			VolumeSource: corev1.VolumeSource{
				Secret: &corev1.SecretVolumeSource{
					SecretName: peerID,
					Items:      []corev1.KeyToPath{{Key: "servercrt", Path: "server.crt"}},
				},
			},
		},
		{
			Name: "serverkey",
			VolumeSource: corev1.VolumeSource{
				Secret: &corev1.SecretVolumeSource{
					SecretName: peerID,
					Items:      []corev1.KeyToPath{{Key: "serverkey", Path: "server.key"}},
				},
			},
		},
		{
			Name: "config",
			VolumeSource: corev1.VolumeSource{
				Secret: &corev1.SecretVolumeSource{
					SecretName: peerID,
					Items:      []corev1.KeyToPath{{Key: "config", Path: "config.yaml"}},
				},
			},
		},
	}
	return volumes
}

func (r *PeerOrgReconciler) objectMetaForCA(peerOrg *fabricv1alpha1.PeerOrg) metav1.ObjectMeta {
	return metav1.ObjectMeta{
		Name:      peerOrg.Name + "-ca",
		Namespace: peerOrg.Namespace,
	}
}

func (r *PeerOrgReconciler) objectMetaForPeer(peerOrg *fabricv1alpha1.PeerOrg) metav1.ObjectMeta {
	return metav1.ObjectMeta{
		Name:      peerOrg.Name + "-peer",
		Namespace: peerOrg.Namespace,
	}
}

func (r *PeerOrgReconciler) containerForCA(peerOrg *fabricv1alpha1.PeerOrg, caID string) corev1.Container {
	container := corev1.Container{
		Name:            caID,
		Image:           "hyperledger/fabric-ca:latest",
		ImagePullPolicy: corev1.PullIfNotPresent,
		Command:         []string{"fabric-ca-server", "start", "-b", "admin:adminpw"},
		Ports: []corev1.ContainerPort{
			{
				Name:          "http",
				ContainerPort: 7054,
			},
		},
	}
	return container
}

func (r *PeerOrgReconciler) containerForPeer(peerOrg *fabricv1alpha1.PeerOrg) corev1.Container {
	container := corev1.Container{
		Name:            "peer",
		Image:           "hyperledger/fabric-peer:2.2",
		ImagePullPolicy: corev1.PullIfNotPresent,
		Ports: []corev1.ContainerPort{
			{
				Name:          "grpc",
				ContainerPort: 7051,
				Protocol:      corev1.ProtocolTCP,
			},
		},
		Env:          r.envForPeer(peerOrg),
		VolumeMounts: r.volumeMountForPeer(),
	}
	return container
}

func (r *PeerOrgReconciler) envForPeer(peerOrg *fabricv1alpha1.PeerOrg) []corev1.EnvVar {
	envs := []corev1.EnvVar{
		{Name: "CORE_PEER_ID", Value: "peer-" + peerOrg.Name},
		{Name: "CORE_PEER_NETWORKID", Value: "net"},
		{Name: "CORE_PEER_LISTENADDRESS", Value: "0.0.0.0:7051"},
		{Name: "CORE_PEER_LOCALMSPID", Value: peerOrg.Spec.ID},
		{Name: "CORE_VM_ENDPOINT", Value: "unix:///host/var/run/docker.sock"},
		{Name: "FABRIC_LOGGING_SPEC", Value: "INFO"},
		{Name: "CORE_PEER_TLS_ENABLED", Value: "true"},
		{Name: "CORE_PEER_GOSSIP_USELEADERELECTION", Value: "true"},
		{Name: "CORE_PEER_GOSSIP_ORGLEADER", Value: "false"},
		{Name: "CORE_PEER_MSPCONFIGPATH", Value: "/etc/hyperledger/fabric/peer/msp"},
		{Name: "CORE_PEER_TLS_CERT_FILE", Value: "/etc/hyperledger/fabric/peer/tls/signcert/server.crt"},
		{Name: "CORE_PEER_TLS_KEY_FILE", Value: "/etc/hyperledger/fabric/peer/tls/keystore/server.key"},
		{Name: "CORE_PEER_TLS_ROOTCERT_FILE", Value: "/etc/hyperledger/fabric/peer/tls/ca.crt"},
	}
	return envs
}

func (r *PeerOrgReconciler) volumeMountForPeer() []corev1.VolumeMount {
	volumes := []corev1.VolumeMount{
		{Name: "signcert", MountPath: "/etc/hyperledger/fabric/peer/msp/signcerts"},
		{Name: "keycert", MountPath: "/etc/hyperledger/fabric/peer/msp/keystore"},
		{Name: "cacert", MountPath: "/etc/hyperledger/fabric/peer/msp/cacerts"},
		{Name: "tlscacert", MountPath: "/etc/hyperledger/fabric/peer/msp/tlscacerts"},
		{Name: "admincerts", MountPath: "/etc/hyperledger/fabric/peer/msp/admincerts"},
		{Name: "cacrt", MountPath: "/etc/hyperledger/fabric/peer/tls"},
		{Name: "servercrt", MountPath: "/etc/hyperledger/fabric/peer/tls/signcert"},
		{Name: "serverkey", MountPath: "/etc/hyperledger/fabric/peer/tls/keystore"},
		{Name: "config", MountPath: "/etc/hyperledger/fabric/peer/msp"},
	}
	return volumes
}

func (r *PeerOrgReconciler) labelsForCA(name string) map[string]string {
	return map[string]string{
		"app.kubernetes.io/name":     name,
		"app.kubernetes.io/instance": name,
	}
}

func (r *PeerOrgReconciler) labelsForPeer(name string) map[string]string {
	return map[string]string{
		"app.kubernetes.io/name":     name,
		"app.kubernetes.io/instance": name,
	}
}

func getPeerHostname(peerOrg *fabricv1alpha1.PeerOrg, peerID string) string {
	return fmt.Sprintf("%s-%s.%s", peerOrg.Name, peerID, peerOrg.Namespace)
}

// SetupWithManager sets up the controller with the Manager.
func (r *PeerOrgReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&fabricv1alpha1.PeerOrg{}).
		Owns(&fabricv1alpha1.PeerOrg{}).
		Complete(r)
}
